/*
 * Copyright (C) 2015-present, osfans
 * waxaca@163.com https://github.com/osfans
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.osfans.trime;

import android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.DisplayMetrics;
import android.view.KeyEvent;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * 从YAML中加载键盘配置，包含多个{@link Key 按键}
 */
public class Keyboard {
    public static final int EDGE_LEFT = 0x01;
    public static final int EDGE_RIGHT = 0x02;
    public static final int EDGE_TOP = 0x04;
    public static final int EDGE_BOTTOM = 0x08;
    private static final int GRID_WIDTH = 10;
    private static final int GRID_HEIGHT = 5;
    private static final int GRID_SIZE = GRID_WIDTH * GRID_HEIGHT;
    private static final String TAG = Keyboard.class.getSimpleName();
    /**
     * Number of key widths from current touch point to search for nearest keys.
     */
    public static float SEARCH_DISTANCE = 1.4f;
    /**
     * 按键默认水平间距
     */
    private int mDefaultHorizontalGap;
    /**
     * 默认单键宽
     */
    private int mDefaultWidth;
    /**
     * 默认单键高
     */
    private int mDefaultHeight;
    /**
     * 默认行距
     */
    private int mDefaultVerticalGap;
    /**
     * 默认按键圆角半径
     */
    private float mRoundCorner;
    /**
     * 键盘背景
     */
    private Drawable mBackground;
    /**
     * 键盘的Shift键是否按住
     */
    private boolean mShifted;
    /**
     * 键盘的Shift键
     */
    private Key mShiftKey;
    /**
     * Total height of the keyboard, including the padding and keys
     */
    private int mTotalHeight;
    /**
     * Total width of the keyboard, including left side gaps and keys, but not any gaps on the right
     * side.
     */
    private int mTotalWidth;
    /**
     * List of keys in this keyboard
     */
    private final List<Key> mKeys;

    private final List<Key> mComposingKeys;
    private int mMetaState;
    /**
     * Width of the screen available to fit the keyboard
     */
    private int mDisplayWidth;
    /**
     * Keyboard mode, or zero, if none.
     */
    private int mAsciiMode;

    // Variables for pre-computing nearest keys.
    private String mLabelTransform;
    private int mCellWidth;
    private int mCellHeight;
    private int[][] mGridNeighbors;
    private int mProximityThreshold;

    private boolean mLock; //切換程序時記憶鍵盤
    private String mAsciiKeyboard; //英文鍵盤

    /**
     * Creates a keyboard from the given xml key layout file.
     *
     * @param context the application or service context
     */
    public Keyboard(Context context) {
        DisplayMetrics dm = context.getResources().getDisplayMetrics();
        mDisplayWidth = dm.widthPixels;
        /* Height of the screen */
        int mDisplayHeight = dm.heightPixels;
        //XLog.v(TAG, "keyboard's display metrics:" + dm);

        Config config = Config.get(context);
        mDefaultHorizontalGap = config.getPixel("horizontal_gap");
        mDefaultVerticalGap = config.getPixel("vertical_gap");
        // key_width 以100为总量配置
        mDefaultWidth = (int) (mDisplayWidth * config.getDouble("key_width") / 100);
        mDefaultHeight = config.getPixel("key_height");
        mProximityThreshold = (int) (mDefaultWidth * SEARCH_DISTANCE);
        mProximityThreshold = mProximityThreshold * mProximityThreshold; // Square it for comparison
        mRoundCorner = config.getFloat("round_corner");
        mBackground = config.getColorDrawable("keyboard_back_color");

        mKeys = new ArrayList<>();
        mComposingKeys = new ArrayList<>();
    }

    /**
     * Creates a blank keyboard from the given resource file and populates it with the specified
     * characters in left-to-right, top-to-bottom fashion, using the specified number of columns.
     *
     * <p>
     *
     * <p>If the specified number of columns is -1, then the keyboard will fit as many keys as
     * possible in each row.
     *
     * @param context           the application or service context
     * @param characters        the list of characters to display on the keyboard. One key will be created
     *                          for each character.
     * @param columns           the number of columns of keys to display. If this number is greater than the
     *                          number of keys that can fit in a row, it will be ignored. If this number is -1, the
     *                          keyboard will fit as many keys as possible in each row.
     * @param horizontalPadding 按鍵水平間距
     */
    public Keyboard(Context context, CharSequence characters, int columns, int horizontalPadding) {
        this(context);
        int x = 0;
        int y = 0;
        int column = 0;
        mTotalWidth = 0;

        final int maxColumns = columns == -1 ? Integer.MAX_VALUE : columns;
        for (int i = 0; i < characters.length(); i++) {
            char c = characters.charAt(i);
            if (column >= maxColumns || x + mDefaultWidth + horizontalPadding > mDisplayWidth) {
                x = 0;
                y += mDefaultVerticalGap + mDefaultHeight;
                column = 0;
            }
            final Key key = new Key(this);
            key.setX(x);
            key.setY(y);
            key.setWidth(mDefaultWidth);
            key.setHeight(mDefaultHeight);
            key.setGap(mDefaultHorizontalGap);
            key.events[0] = new Event(this, String.valueOf(c));
            column++;
            x += key.getWidth() + key.getGap();
            mKeys.add(key);
            if (x > mTotalWidth) {
                mTotalWidth = x;
            }
        }
        mTotalHeight = y + mDefaultHeight;
    }

    public Keyboard(Context context, String name) {
        this(context);
        Map<String, Object> m = Config.get(context).getKeyboard(name);
        mLabelTransform = Config.getString(m, "label_transform", "none");
        mAsciiMode = Config.getInt(m, "ascii_mode", 1);
        if (mAsciiMode == 0) mAsciiKeyboard = Config.getString(m, "ascii_keyboard");
        mLock = Config.getBoolean(m, "lock", false);
        int columns = Config.getInt(m, "columns", 30);
        int defaultWidth = (int) (Config.getDouble(m, "width", 0) * mDisplayWidth / 100);
        if (defaultWidth == 0) defaultWidth = mDefaultWidth;
        int height = Config.getPixel(m, "height", 0);
        int defaultHeight = (height > 0) ? height : mDefaultHeight;
        int rowHeight = defaultHeight;
        List<Map<String, Object>> keys = (List<Map<String, Object>>) m.get("keys");

        if (m.containsKey("horizontal_gap"))
            mDefaultHorizontalGap = Config.getPixel(m, "horizontal_gap");
        if (m.containsKey("vertical_gap")) mDefaultVerticalGap = Config.getPixel(m, "vertical_gap");
        if (m.containsKey("round_corner")) mRoundCorner = Config.getFloat(m, "round_corner");
        if (m.containsKey("keyboard_back_color")) {
            Drawable background = Config.getColorDrawable(context, m, "keyboard_back_color");
            if (background != null) mBackground = background;
        }
        int x = mDefaultHorizontalGap / 2;
        int y = mDefaultVerticalGap;
        int row = 0;
        int column = 0;
        mTotalWidth = 0;
        int key_text_offset_x,
                key_text_offset_y,
                key_symbol_offset_x,
                key_symbol_offset_y,
                key_hint_offset_x,
                key_hint_offset_y,
                key_press_offset_x,
                key_press_offset_y;
        key_text_offset_x = Config.getPixel(m, "key_text_offset_x", 0);
        key_text_offset_y = Config.getPixel(m, "key_text_offset_y", 0);
        key_symbol_offset_x = Config.getPixel(m, "key_symbol_offset_x", 0);
        key_symbol_offset_y = Config.getPixel(m, "key_symbol_offset_y", 0);
        key_hint_offset_x = Config.getPixel(m, "key_hint_offset_x", 0);
        key_hint_offset_y = Config.getPixel(m, "key_hint_offset_y", 0);
        key_press_offset_x = Config.getInt(m, "key_press_offset_x", 0);
        key_press_offset_y = Config.getInt(m, "key_press_offset_y", 0);

        final int maxColumns = columns == -1 ? Integer.MAX_VALUE : columns;
        for (Map<String, Object> singleKey : Objects.requireNonNull(keys)) {
            int gap = mDefaultHorizontalGap;
            int singleKeyWidth = (int) (mDisplayWidth * Config.getDouble(singleKey, "width", 0) / 100);
            if (singleKeyWidth == 0 && singleKey.containsKey("click")) singleKeyWidth = defaultWidth;
            singleKeyWidth -= gap;
            if (column >= maxColumns || x + singleKeyWidth > mDisplayWidth) {
                x = gap / 2;
                y += mDefaultVerticalGap + rowHeight;
                column = 0;
                row++;
                if (mKeys.size() > 0) mKeys.get(mKeys.size() - 1).edgeFlags |= Keyboard.EDGE_RIGHT;
            }
            if (column == 0) {
                int heightK = Config.getPixel(singleKey, "height", 0);
                rowHeight = (heightK > 0) ? heightK : defaultHeight;
            }
            if (!singleKey.containsKey("click")) { //無按鍵事件
                x += singleKeyWidth + gap;
                continue; //縮進
            }

            final Key key = new Key(context, this, singleKey);
            key.setKey_text_offset_x(Config.getPixel(singleKey, "key_text_offset_x", key_text_offset_x));
            key.setKey_text_offset_y(Config.getPixel(singleKey, "key_text_offset_y", key_text_offset_y));
            key.setKey_symbol_offset_x(Config.getPixel(singleKey, "key_symbol_offset_x", key_symbol_offset_x));
            key.setKey_symbol_offset_y(Config.getPixel(singleKey, "key_symbol_offset_y", key_symbol_offset_y));
            key.setKey_hint_offset_x(Config.getPixel(singleKey, "key_hint_offset_x", key_hint_offset_x));
            key.setKey_hint_offset_y(Config.getPixel(singleKey, "key_hint_offset_y", key_hint_offset_y));
            key.setKey_press_offset_x(Config.getInt(singleKey, "key_press_offset_x", key_press_offset_x));
            key.setKey_press_offset_y(Config.getInt(singleKey, "key_press_offset_y", key_press_offset_y));

            key.setX(x);
            key.setY(y);
            int right_gap = Math.abs(mDisplayWidth - x - singleKeyWidth - gap / 2);
            //右側不留白
            key.setWidth((right_gap <= mDisplayWidth / 100) ? mDisplayWidth - x - gap / 2 : singleKeyWidth);
            key.setHeight(rowHeight);
            key.setGap(gap);
            key.setRow(row);
            key.setColumn(column);
            column++;
            x += key.getWidth() + key.getGap();
            mKeys.add(key);
            if (x > mTotalWidth) {
                mTotalWidth = x;
            }
        }
        if (mKeys.size() > 0) mKeys.get(mKeys.size() - 1).edgeFlags |= Keyboard.EDGE_RIGHT;
        mTotalHeight = y + rowHeight + mDefaultVerticalGap;
        for (Key key : mKeys) {
            if (key.getColumn() == 0) key.edgeFlags |= Keyboard.EDGE_LEFT;
            if (key.getRow() == 0) key.edgeFlags |= Keyboard.EDGE_TOP;
            if (key.getRow() == row) key.edgeFlags |= Keyboard.EDGE_BOTTOM;
        }
    }

    public Key getmShiftKey() {
        return mShiftKey;
    }

    public void setmShiftKey(Key mShiftKey) {
        this.mShiftKey = mShiftKey;
    }

    public List<Key> getmComposingKeys() {
        return mComposingKeys;
    }

    public List<Key> getKeys() {
        return mKeys;
    }

    public List<Key> getComposingKeys() {
        return mComposingKeys;
    }

    protected int getHorizontalGap() {
        return mDefaultHorizontalGap;
    }

    protected void setHorizontalGap(int gap) {
        mDefaultHorizontalGap = gap;
    }

    protected int getVerticalGap() {
        return mDefaultVerticalGap;
    }

    protected void setVerticalGap(int gap) {
        mDefaultVerticalGap = gap;
    }

    protected int getKeyHeight() {
        return mDefaultHeight;
    }

    protected void setKeyHeight(int height) {
        mDefaultHeight = height;
    }

    protected int getKeyWidth() {
        return mDefaultWidth;
    }

    protected void setKeyWidth(int width) {
        mDefaultWidth = width;
    }

    /**
     * Returns the total height of the keyboard
     *
     * @return the total height of the keyboard
     */
    public int getHeight() {
        return mTotalHeight;
    }

    public int getMinWidth() {
        return mTotalWidth;
    }

    private boolean hasModifier(int modifiers) {
        return (mMetaState & modifiers) != 0;
    }

    public boolean hasModifier() {
        return mMetaState != 0;
    }

    public boolean toggleModifier(int mask) {
        boolean value = !hasModifier(mask);
        if (value) mMetaState |= mask;
        else mMetaState &= ~mask;
        return value;
    }

    public int getModifer() {
        return mMetaState;
    }

    private boolean setModifier(int mask, boolean value) {
        boolean b = hasModifier(mask);
        if (b == value) return false;
        if (value) mMetaState |= mask;
        else mMetaState &= ~mask;
        return true;
    }

    public boolean isAlted() {
        return hasModifier(KeyEvent.META_ALT_ON);
    }

    public boolean isShifted() {
        return hasModifier(KeyEvent.META_SHIFT_ON);
    }

    public boolean isCtrled() {
        return hasModifier(KeyEvent.META_CTRL_ON);
    }

    /**
     * 設定鍵盤的Shift鍵狀態
     *
     * @param on      是否保持Shift按下狀態
     * @param shifted 是否按下Shift
     * @return Shift鍵狀態是否改變
     */
    public boolean setShifted(boolean on, boolean shifted) {
        on = on & shifted;
        if (mShiftKey != null) mShiftKey.setOn(on);
        return setModifier(KeyEvent.META_SHIFT_ON, on || shifted);
    }

    public boolean resetShifted() {
        if (mShiftKey != null && !mShiftKey.isOn())
            return setModifier(KeyEvent.META_SHIFT_ON, false);
        return false;
    }

    private void computeNearestNeighbors() {
        // Round-up so we don't have any pixels outside the grid
        mCellWidth = (getMinWidth() + GRID_WIDTH - 1) / GRID_WIDTH;
        mCellHeight = (getHeight() + GRID_HEIGHT - 1) / GRID_HEIGHT;
        mGridNeighbors = new int[GRID_SIZE][];
        int[] indices = new int[mKeys.size()];
        final int gridWidth = GRID_WIDTH * mCellWidth;
        final int gridHeight = GRID_HEIGHT * mCellHeight;
        for (int x = 0; x < gridWidth; x += mCellWidth) {
            for (int y = 0; y < gridHeight; y += mCellHeight) {
                int count = 0;
                for (int i = 0; i < mKeys.size(); i++) {
                    final Key key = mKeys.get(i);
                    if (key.squaredDistanceFrom(x, y) < mProximityThreshold
                            || key.squaredDistanceFrom(x + mCellWidth - 1, y) < mProximityThreshold
                            || key.squaredDistanceFrom(x + mCellWidth - 1, y + mCellHeight - 1)
                            < mProximityThreshold
                            || key.squaredDistanceFrom(x, y + mCellHeight - 1) < mProximityThreshold
                            || key.isInside(x, y)
                            || key.isInside(x + mCellWidth - 1, y)
                            || key.isInside(x + mCellWidth - 1, y + mCellHeight - 1)
                            || key.isInside(x, y + mCellHeight - 1)) {
                        indices[count++] = i;
                    }
                }
                int[] cell = new int[count];
                System.arraycopy(indices, 0, cell, 0, count);
                mGridNeighbors[(y / mCellHeight) * GRID_WIDTH + (x / mCellWidth)] = cell;
            }
        }
    }

    /**
     * Returns the indices of the keys that are closest to the given point.
     *
     * @param x the x-coordinate of the point
     * @param y the y-coordinate of the point
     * @return the array of integer indices for the nearest keys to the given point. If the given
     * point is out of range, then an array of size zero is returned.
     */
    public int[] getNearestKeys(int x, int y) {
        if (mGridNeighbors == null) computeNearestNeighbors();
        if (x >= 0 && x < getMinWidth() && y >= 0 && y < getHeight()) {
            int index = (y / mCellHeight) * GRID_WIDTH + (x / mCellWidth);
            if (index < GRID_SIZE) {
                return mGridNeighbors[index];
            }
        }
        return new int[0];
    }

    public boolean getAsciiMode() {
        return mAsciiMode != 0;
    }

    public String getAsciiKeyboard() {
        return mAsciiKeyboard;
    }

    public boolean isLabelUppercase() {
        return mLabelTransform.contentEquals("uppercase");
    }

    public boolean isLock() {
        return mLock;
    }

    public float getRoundCorner() {
        return mRoundCorner;
    }

    public Drawable getBackground() {
        return mBackground;
    }
}
